/*
 * This code is licensed under the Apache License 2.0.
 * Read more at http://blippit.googlecode.com/
 */
package com.googlecode.blippit;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Iterator;
import java.util.Random;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;


import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.ParseException;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.message.BasicHeaderElement;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.googlecode.blippit.BlockingHttpClient;

import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

public class BlipApi {
	public static final String FUZZ_SONG = "fuzzSong";

	private static final String TAG = "BlipApi";
	private static String apiKey;
	private static Header httpAuthHeader;
	private static String httpAuthUser;
	private static Key secretKey;
	private static String apiHost = "api.blip.fm";
	private static final String FORMAT = ".json";
	private static int apiPort = 80;
	private static long timeOffset = 0;
	private static final String hmacAlgo = "HMacSHA1";
	private static final Random ramdom = new Random();

	static void init(String api_key, String secret_key) {
		apiKey = api_key;
		byte[] key = secret_key.getBytes();
		secretKey = new SecretKeySpec(key, 0, key.length, hmacAlgo);
	}

	static void auth(String user, String password) {
		if (user == null && password == null) {
			httpAuthHeader = null;
			httpAuthUser = null;
		} else {
			byte[] s = (user + ":" + password).getBytes();
			final String name = "Authorization";
			final String value = "Basic " + Base64.encodeBase64(s);
			final BasicHeaderElement he = new BasicHeaderElement(name, value);
			httpAuthHeader = new Header() {
				public HeaderElement[] getElements() throws ParseException {
					return new HeaderElement[] { he };
				}
				public String getName() {
					return name;
				}
				public String getValue() {
					return value;
				}
			};
			httpAuthUser = user;
		}
	}

	static void setTimeOffset(long l) {
		timeOffset = l;
	}

	public static String sign(String verb, String timestamp, String nonce) {
		StringBuffer toSign = new StringBuffer(verb)
		.append('\n')
		.append(timestamp)
		.append('\n')
		.append(nonce);

		Mac hmac;
		try {
			hmac = Mac.getInstance(hmacAlgo);
		} catch (NoSuchAlgorithmException e) {
			return null;
		}
		try {
			hmac.init(secretKey);
		} catch (InvalidKeyException e) {
			return null;
		}
		byte[] result = hmac.doFinal(toSign.toString().getBytes());
		return new String(Base64.encodeBase64(result));
	}

	static ApiResult fetchGet(Uri.Builder uri, Header[] headers) {
		Uri u = uri.build();
		try {
			BlockingHttpClient.Response resp = BlockingHttpClient.get(apiHost, apiPort, u.toString(), headers);
			return ApiResult.fromJson(resp.body);
		} catch (ClientProtocolException e) {
			Log.e(TAG, "get failed: protocol error", e);
			return null;
		} catch (IOException e) {
			Log.e(TAG, "get failed: I/O error", e);
			return null;
		}
	}

	static ApiResult fetchGet(Uri.Builder uri) {
		return fetchGet(uri, null);
	}

	static ApiResult fetchAuthGet(Uri.Builder uri) throws AuthenticationException {
		if (httpAuthHeader == null) {
			throw new AuthenticationException("Must provide credentials");
		}
		Header[] headers = { httpAuthHeader };
		return fetchGet(uri, headers);
	}

	static String fetchPost(Uri.Builder uri) {
		return null;
	}

	static String fetchAuthPost(Uri.Builder uri) {
		return null;
	}

	static String newNonce() {
		StringBuilder sb = new StringBuilder();
		while (sb.length() < 7) {
			sb.append(Long.toString(ramdom.nextLong(), 36));
		}
		return sb.toString();
	}

	static Uri.Builder signGet(Uri.Builder b) {
		String time = Long.toString(timeOffset + new Date().getTime() / 1000);
		String nonce = newNonce();
		b.appendQueryParameter("apiKey", apiKey);
		b.appendQueryParameter("nonce", nonce);
		b.appendQueryParameter("timestamp", time);
		b.appendQueryParameter("signature", sign("GET", time, nonce));
		return b;
	}

	static class ApiResult {
		public int status_code;
		public String status_message;
		public int status_requestTime;
		public int status_responseTime;
		public String error_message;
		Bundle bundle;

		private ApiResult() {}
		public static ApiResult fromJson(String str) {
			ApiResult r = new ApiResult();
			JSONObject o;
			try {
				o = new JSONObject(str);
			} catch (JSONException e) {
				Log.e(TAG, "JSON parse failed", e);
				return null;
			}
			JSONObject status = o.optJSONObject("status");
			if (status != null) {
				r.status_code = status.optInt("code");
				r.status_message = status.optString("message");
				r.status_requestTime = status.optInt("requestTime");
				r.status_responseTime = status.optInt("responseTime");
			}
			JSONObject error = o.optJSONObject("error");
			if (error != null) {
				r.error_message = status.optString("message");
			}
			r.bundle = new Bundle();
			fillBundle(r.bundle, o);
			return r;
		}

		@SuppressWarnings("unchecked")
		private static void fillBundle(Bundle b, JSONObject o) {
			for (Iterator i = o.keys(); i.hasNext(); ) {
				String key = (String) i.next();
				putBundleObject(b, key, o.opt(key));
			}
		}

		private static void putBundleObject(Bundle b, String key, Object val) {
			if (val instanceof JSONObject) {
				JSONObject oj = (JSONObject)val;
				Bundle nb = new Bundle();
				//				Log.v(TAG, "JSON: " + key + " = {");
				fillBundle(nb, oj);
				//				Log.v(TAG, "JSON: }");
				b.putBundle(key, nb);
			} else if (val instanceof String) {
				// Also hides: int, float, boolean
				//				Log.v(TAG, "JSON: " + key + " = " + val.toString());
				b.putString(key, (String)val);
			} else if (val instanceof JSONArray) {
				JSONArray a = (JSONArray) val;
				b.putInt(key + ".count", a.length());
				for (int i = 0; i < a.length(); i++) {
					// This greatly simplifies the filling of the bundle.
					// Also, bundles can't contain arrays of bundles (though
					// they can contain arrays of parcelables)
					putBundleObject(b, key + "." + i, a.opt(i));
				}
			} else {
				Log.e(TAG, "Unhandled JSON: " + key + " of type " + val.getClass().getName());
			}
		}
	}

	interface Song {
		String url();
		String artist();
		String title();
		String type();
	}
	interface Blip extends Song {
		int id();
		String url();
		int ownerId();
		String insTime();
		String message();
		int unixTime();
		String status();
		String ownerUrlName();
		// Unused: toId, owner
	}

	static String assembleBlipTitle(Blip b) {
		StringBuffer sb = new StringBuffer(b.ownerUrlName());
		String m = b.message().trim();
		if (m.length() > 0) {
			sb.append("  ").append(m);
		}
		return sb.toString();
	}

	static Blip toBlip(final Bundle blip) {
		return new Blip() {
			@Override
			public String toString() {
				return assembleBlipTitle(this);
			}
			public String artist() {
				return blip.getString("artist");
			}
			public String type() {
				return blip.getString("type");
			}
			public String url() {
				return blip.getString("url");
			}
			public String title() {
				return blip.getString("title");
			}
			public int id() {
				return Integer.parseInt(blip.getString("id"));
			}
			public String insTime() {
				return blip.getString("insTime");
			}
			public String message() {
				return blip.getString("message");
			}
			public int ownerId() {
				return Integer.parseInt(blip.getString("ownerId"));
			}
			public String ownerUrlName() {
				String u = blip.getString("ownerUrlName");
				if (u != null)
					return u;
				Bundle o = blip.getBundle("owner");
				if (o != null)
					return o.getString("urlName");
				return "nobody";
			}
			public String status() {
				return blip.getString("status");
			}
			public int unixTime() {
				return Integer.parseInt(blip.getString("unixTime"));
			}
		};
	}

	static String guessSongTitle(Song s) {
		String t = s.title();
		if (t != null && t.length() > 0)
			return t;
		String url = s.url();
		if (s.type().equals("songUrl")) {
			Uri u = Uri.parse(url);
			return u.getLastPathSegment();
		} else if (s.type().equals("fuzzSong")) {
			return url.split(" ")[1];
		} else {
			return "";
		}
	}

	static Song toSong(final Bundle song) {
		return new Song() {
			@Override
			public String toString() {
				return guessSongTitle(this);
			}
			public String artist() {
				return song.getString("artist");
			}
			public String type() {
				return song.getString("blipType");
			}
			public String url() {
				return song.getString("location");
			}
			public String title() {
				return song.getString("title");
			}
		};
	}

	static Blip[] toBlipArray(Bundle b) {
		if (b == null) {
			return null;
		}
		Bundle result = b.getBundle("result");
		if (result == null) {
			Log.w(TAG, "No result in bundle " + b);
			return null;
		}
		Log.i(TAG, "toBlipArray: result = " + result);
		Bundle collection = result.getBundle("collection");
		if (collection == null) {
			Log.w(TAG, "No collection in result " + result);
			return null;
		}
		int count = collection.getInt("Blip.count");
		Blip[] a = new Blip[count];
		for (int i = 0; i < count; i++) {
			a[i] = toBlip(collection.getBundle("Blip." + i));
		}
		return a;
	}

	static Song[] toSongArray(Bundle b) {
		if (b == null) {
			return null;
		}
		Bundle result = b.getBundle("result");
		if (result == null) {
			Log.w(TAG, "No result in bundle " + b);
			return null;
		}
		Log.i(TAG, "toSongArray: result = " + result);
		Bundle collection = result.getBundle("collection");
		if (collection == null) {
			Log.w(TAG, "No collection in result " + result);
			return null;
		}
		int count = collection.getInt("Song.count");
		Song[] a = new Song[count];
		for (int i = 0; i < count; i++) {
			a[i] = toSong(collection.getBundle("Song." + i));
		}
		return a;
	}

	static class BlipService {
		static final String base = "/blip";

		/**
		Delete a blip. The authorized user must be the owner of the blip.

		URL: http://api.blip.fm/blip/delete (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Deleted blip on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2Fdelete

		@param id 	The id of the blip to be deleted (required)
		 * @throws AuthenticationException 
		 */
		static ApiResult delete(int id) throws AuthenticationException {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("delete" + FORMAT)
			.appendQueryParameter("id", Integer.toString(id));
			return fetchAuthGet(signGet(b));
		}

		/**
		Use this to get a Blip by id.

		URL: http://api.blip.fm/blip/getById (xml | json | php)
		Method: GET
		Requires API Key?: no
		Requires Authentication?: no
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetById

		@param id 	The id(s) of the blip(s) you want to fetch. Required. int | array 	-
		 */
		static ApiResult getById(int id) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getById" + FORMAT)
			.appendQueryParameter("id", Integer.toString(id));

			return fetchGet(b);
		}

		/**
		Get public blips (the all blips page) ordered by recency.

		URL: http://api.blip.fm/blip/getPublic (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: optional
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetPublic
		Name 	Description 	Required? 	Type 	Default
		offset 	The offset you wanna get blips at. 	no 	int 	0
		limit 	How many blips do you wanna get? 	no 	int 	25
		 */
		static ApiResult getPublic(Integer offset, Integer count) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getPublic" + FORMAT)
			.appendQueryParameter("apiKey", apiKey);
			if (offset != null)
				b.appendQueryParameter("offset", offset.toString());
			if (count != null)
				b.appendQueryParameter("count", count.toString());

			return fetchGet(signGet(b));
		}

		/**
		Get a user's personal station blips (blips from people they are following).

		URL: http://api.blip.fm/blip/getUserHome (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetUserHome
		Name 	Description 	Required? 	Type 	Default
		offset 	The offset you wanna get stuff at. 	no 	int 	0
		limit 	How many Blips do you wanna get? 	no 	int 	25
		 * @throws AuthenticationException 
		 */
		static ApiResult getUserHome(Integer offset, Integer count) throws AuthenticationException {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserHome" + FORMAT)
			.appendQueryParameter("apiKey", apiKey);
			if (offset != null)
				b.appendQueryParameter("offset", offset.toString());
			if (count != null)
				b.appendQueryParameter("count", count.toString());

			return fetchAuthGet(signGet(b));
		}

		/**
		Get a user's playlist blips.

		URL: http://api.blip.fm/blip/getUserPlaylist (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: optional
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetUserPlaylist
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose playlist you want to get. 	yes 	string 	-
		 */
		static ApiResult getUserPlaylist(String username) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserPlaylist" + FORMAT)
			.appendQueryParameter("apiKey", apiKey)
			.appendQueryParameter("username", username);

			return fetchGet(signGet(b));
		}

		/**
		/blip/getUserProfile

		Description: Get Blips for a User ordered by recency.

		URL: http://api.blip.fm/blip/getUserProfile (xml | json | php)
		Method: GET
		Requires API Key?: no
		Requires Authentication?: optional
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetUserProfile
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose Blips you wanna get. 	yes 	string 	-
		offset 	The offset you wanna get stuff at. 	no 	int 	0
		limit 	How many Blips do you wanna get? 	no 	int 	25
		 */
		static ApiResult getUserProfile(String username, Integer offset, Integer count) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserProfile" + FORMAT)
			.appendQueryParameter("username", username);
			if (offset != null)
				b.appendQueryParameter("offset", offset.toString());
			if (count != null)
				b.appendQueryParameter("count", count.toString());

			return fetchGet(b);
		}

		/**
		/blip/getUserReplies

		Description: Get blips that have replies to a user.

		URL: http://api.blip.fm/blip/getUserReplies (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: optional
		Returns: Collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2FgetUserReplies
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose replies you want to get. 	yes 	string 	-
		 */
		static ApiResult getUserReplies(String username) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserReplies" + FORMAT)
			.appendQueryParameter("apiKey", apiKey)
			.appendQueryParameter("username", username);

			return fetchGet(signGet(b));
		}

		/**
		/blip/ping

		Description: Get blips posted after a given time.

		URL: http://api.blip.fm/blip/ping (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: optional
		Returns: Current timestamp and a collection of blips on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2Fping
		Name 	Description 	Required? 	Type 	Default
		ts 	Unix timestamp (in seconds). Blips posted after this timestamp will be returned. 	yes 	int 	-
		station 	The station ('home' or 'public') you want to ping. Authentication is required if this is set to 'home'. 	yes 	string 	home
		offset 	The offset you wanna get stuff at. 	no 	int 	0
		limit 	How many Blips do you wanna get? 	no 	int 	25
		 * @throws AuthenticationException 
		 */
		static ApiResult ping(int ts, String station, Integer offset, Integer count) throws AuthenticationException {
			boolean needAuth = ("home".equals(station));
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserProfile" + FORMAT)
			.appendQueryParameter("ts", Integer.toString(ts))
			.appendQueryParameter("station", station);
			if (offset != null)
				b.appendQueryParameter("offset", offset.toString());
			if (count != null)
				b.appendQueryParameter("count", count.toString());
			if (needAuth)
				return fetchAuthGet(signGet(b));
			else
				return fetchGet(signGet(b));
		}

		/**
		/blip/post

		Description: Post a blip.

		URL: http://api.blip.fm/blip/post (xml | json | php)
		Method: POST
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Blip that was just posted and users to recommend on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=BlipService&endpoint=%2Fblip%2Fpost
		Name 	Description 	Required? 	Type 	Default
		artist 	The artist of the song to be blipped. 	yes 	string 	-
		title 	The title of the song to be blipped. 	yes 	string 	-
		url 	The url or unique identifier of the song to be blipped. 	yes 	string 	-
		message 	The message attached to the song to be blipped. 	yes 	string 	-
		blipType 	The type of the blip. 	yes 	string 	songUrl
		 * @throws AuthenticationException 
		 */
		static ApiResult post(String artist, String title, String url, String message, String blipType) throws AuthenticationException {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("getUserProfile" + FORMAT)
			.appendQueryParameter("artist", artist)
			.appendQueryParameter("title", title)
			.appendQueryParameter("url", url)
			.appendQueryParameter("message", message)
			.appendQueryParameter("blipType", blipType);
			return fetchAuthGet(signGet(b));
		}
	}


	static class FavoriteService {
		static final String base = "/favorite";

		/**
		/favorite/addDJ

		Description: Add a user as a favorite DJ.

		URL: http://api.blip.fm/favorite/addDJ (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Favorited User on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=FavoriteService&endpoint=%2Ffavorite%2FaddDJ
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the DJ to add as a favorite. 	yes 	string 	-
		 */


		/**
		/favorite/removeDJ

		Description: Remove a user as a favorite DJ.

		URL: http://api.blip.fm/favorite/removeDJ (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Removed user on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=FavoriteService&endpoint=%2Ffavorite%2FremoveDJ
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the DJ to remove as a favorite. 	yes 	string 	-
		 */
	}

	static class SearchService {
		static final String base = "/search";

		/**
		/search/findSongs

		Description: Find songs by song name or artist.

		URL: http://api.blip.fm/search/findSongs (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: no
		Returns: Collection of songs on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=SearchService&endpoint=%2Fsearch%2FfindSongs
		Name 	Description 	Required? 	Type 	Default
		searchTerm 	The song or artist to search for. 	yes 	string 	-
		 */
		static ApiResult findSongs(String searchTerm) {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("findSongs" + FORMAT)
			.appendQueryParameter("apiKey", apiKey)
			.appendQueryParameter("searchTerm", searchTerm);

			return fetchGet(signGet(b));
		}
	}

	static class UserService {

		/**
		/user/getByUsername

		Description: Use this to get a User by username. Note: urlName and username are effectively synonymous.

		URL: http://api.blip.fm/user/getByUsername (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: yes
		Returns: Collection of users on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=UserService&endpoint=%2Fuser%2FgetByUsername
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User(s) you want to fetch 	yes 	string | array 	-
		 */


		/**
		/user/getFavoriteDJs

		Description: Get the User's favorite DJs.

		URL: http://api.blip.fm/user/getFavoriteDJs (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: no
		Returns: Collection of users on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=UserService&endpoint=%2Fuser%2FgetFavoriteDJs
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose favorite DJs you want to get. 	yes 	string 	-
		offset 	The offset you wanna get stuff at. 	no 	int 	0
		limit 	How many favorite DJs do you want to get (default 25)? 	no 	int 	25
		 */


		/**
		/user/getListeners

		Description: Get the User's listeners.

		URL: http://api.blip.fm/user/getListeners (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: no
		Returns: Collection of users on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=UserService&endpoint=%2Fuser%2FgetListeners
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose listeners you want to get. 	yes 	string 	-
		offset 	The offset you wanna get stuff at. 	no 	int 	0
		limit 	How many listeners do you want to get (default 25)? 	no 	int 	25
		 */


		/**
		/user/getStats

		Description: Get a user's stats - # blips in playlist, # previous blips, # replies received, # props received, # favorite djs, # listeners.

		URL: http://api.blip.fm/user/getStats (xml | json | php)
		Method: GET
		Requires API Key?: yes
		Requires Authentication?: no
		Returns: Stats object on success. Error message on failure.
		API Tools URL: http://api.blip.fm/apiTools?service=UserService&endpoint=%2Fuser%2FgetStats
		Name 	Description 	Required? 	Type 	Default
		username 	The username of the User whose stats you want to get. 	yes 	string 	-
		 */
	}

	/**
	UtilService top
	 */

	static class UtilService {
		static final String base = "/util";

		/**
		/util/hello

		Description: Check if the server's responding.

		URL: http://api.blip.fm/util/hello (xml | json | php)
		Method: GET
		Requires API Key?: no
		Requires Authentication?: no
		Returns: string
		API Tools URL: http://api.blip.fm/apiTools?service=UtilService&endpoint=%2Futil%2Fhello
		Name 	Description 	Required? 	Type 	Default
		No parameters
		 */
		static ApiResult hello() {
			Uri.Builder b = new Uri.Builder()
			.path(base)
			.appendPath("hello" + FORMAT);
			return fetchGet(b);
		}
	}

	public static String getUsername() {
		return httpAuthUser;
	}
}
